---
title: 编辑帖子
slug: editing-posts
date: 0008/01/01
number: 8
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9473337133/
photoAuthor: Mike Lewinski
contents: 添加一个帖子编辑表单。|设置可编辑权限。|限制哪些属性可以编辑。
paragraphs: 29
---

上一章，我们已经学会了创建帖子，下面来学习编辑和删除它们。页面的代码非常简单，让我们在这个时候来谈论一下 Meteor 是如何管理用户权限。

让我们先设置我们的路由器，添加一个可以访问帖子编辑页的路径，并设置它的数据上下文：

~~~js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  notFoundTemplate: 'notFound',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.route('/', {name: 'postsList'});

Router.route('/posts/:_id', {
  name: 'postPage',
  data: function() { return Posts.findOne(this.params._id); }
});

Router.route('/posts/:_id/edit', {
  name: 'postEdit',
  data: function() { return Posts.findOne(this.params._id); }
});

Router.route('/submit', {name: 'postSubmit'});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn()) {
      this.render(this.loadingTemplate);
    } else {
      this.render('accessDenied');
    }
  } else {
    this.next();
  }
}

Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~18" %>

### 帖子编辑模板

我们可以现在专注模板了。我们的 `postEdit` 模板就包含一个相当标准的表单：

~~~html
<template name="postEdit">
  <form class="main form">
    <div class="form-group">
      <label class="control-label" for="url">URL</label>
      <div class="controls">
          <input name="url" id="url" type="text" value="{{url}}" placeholder="Your URL" class="form-control"/>
      </div>
    </div>
    <div class="form-group">
      <label class="control-label" for="title">Title</label>
      <div class="controls">
          <input name="title" id="title" type="text" value="{{title}}" placeholder="Name your post" class="form-control"/>
      </div>
    </div>
    <input type="submit" value="Submit" class="btn btn-primary submit"/>
    <hr/>
    <a class="btn btn-danger delete" href="#">Delete post</a>
  </form>
</template>
~~~
<%= caption "client/templates/posts/post_edit.html" %>

用 `post_edit.js` 来配合这个的模板：

~~~js
Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();
    
    var currentPostId = this._id;
    
    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }
    
    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // 向用户显示错误信息
        alert(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },
  
  'click .delete': function(e) {
    e.preventDefault();
    
    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('postsList');
    }
  }
});
~~~
<%= caption "client/templates/posts/post_edit.js" %>

相信你现在已经对这些代码都相当的熟悉了。

我们有两个事件回调函数：一个用于表单的 `submit` 事件，一个用于删除链接的 `click` 事件。

删除链接的回调函数是非常简单的：先防止默认点击事件，然后提示确认窗口。如果确认删除，它将从模板的数据上下文中获得当前帖子的 ID ，然后删除它，最后把用户重定向到主页。

更新的回调函数需要长一点时间，但并不复杂。在防止默认提交事件然后获取了当前帖子之后，我们将从表单中获取相关字段的值，并将它们存储在一个 `postProperties` 的对象中。

然后，我们把该对象通过 [`$set`](http://docs.mongodb.org/manual/reference/operator/update/set/) 操作符（只更新指定字段的值，保留其他字段的值）传递给 Meteor 的 `Collection.update()` 方法，并通过回调函数去判断如果更新失败就显示错误信息；如果更新成功了，将自动返回到该帖子的页面。

### 添加链接

我们还应该添加一个编辑帖子的链接，以便用户可以访问到帖子编辑页面：

~~~html
<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}}
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
  </div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "5~8" %>

当然，我们不能让你的帖子提供给其他用户去编辑。这就要通过 `ownPost` helper 来帮忙：

~~~js
Template.postItem.helpers({
  ownPost: function() {
    return this.userId === Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "2~4" %>

<%= screenshot "8-1", "帖子编辑表单。" %>

<%= commit "8-1", "添加帖子编辑表单。" %>

我们的帖子编辑表单看起来很好，但是目前还不能够进行任何的编辑，这是为什么？

### 设置权限

自从我们移除了 `insecure` 包，现在所有客户端的修改都会被拒绝。

为了解决这个问题，我们需要建立一些权限规则。首先，在 `lib` 目录下创建一个新的 `permissions.js` 文件。这样做将会首先加载我们权限文件（它在服务端和客户端都可以被加载到）：

~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
  return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>

在[创建帖子](/chapter/creating-posts)这个章节，我们抛弃了 `allow()` 方法，因为我们只通过服务端方法去插入新的帖子（绕过了 `allow()` 方法）。

但是现在我们要在客户端编辑和删除帖子！我们回到 `posts.js` 文件并添加 `allow()` ：

~~~js
Posts = new Mongo.Collection('posts');

Posts.allow({
  update: function(userId, post) { return ownsDocument(userId, post); },
  remove: function(userId, post) { return ownsDocument(userId, post); }
});

//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~6" %>

<%= commit "8-2", "添加基本权限来检查帖子的拥有者。" %>

### 限制编辑

尽管你可以编辑自己的帖子，但并不意味着你可以允许去编辑帖子的**每个**属性。例如，我们不允许用户创建一个帖子之后，再将其分配给其他用户。

我们用 Meteor 的 `deny()` 方法，以确保用户只能编辑特定的字段：

~~~js
Posts = new Mongo.Collection('posts');

Posts.allow({
  update: function(userId, post) { return ownsDocument(userId, post); },
  remove: function(userId, post) { return ownsDocument(userId, post); }
});

Posts.deny({
  update: function(userId, post, fieldNames) {
    // 只能更改如下两个字段：
    return (_.without(fieldNames, 'url', 'title').length > 0);
  }
});

//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~13" %>

<%= commit "8-3", "只允许更改帖子的某些属性。" %>

代码中的 `fieldNames` 数组，它包含了需要被修改的字段，并使用  [Underscore](http://underscorejs.org/) 的 `without()` 方法返回一个*不包含* `url` 和 `title` 字段的子数组。

正常情况下，这个数组应该是空的，它的长度应该是0。如果有人采取其他操作，这个数组的长度将变为1或更多，回调函数将返回 `true` （因此禁止更新）。

你也许注意到了在我们的代码中没有检查链接是否重复的代码。这就意味着用户成功添加一个链接后，再编辑时就会绕过检查。这个问题同样可以通过为编辑帖子表单使用 Meteor 内置方法来解决，但是我们将它作为练习留给读者。

<% note do %>

### 内置方法的回调 vs 客户端数据操作

创建帖子，我们使用的是 `postInsert` 的内置方法，而编辑和删除帖子，我们直接在客户端调用 `update` 和 `remove`，并通过 `allow` 和 `deny` 去限制使用权限。

我们该如何去选择使用呢？

当操作相对比较直观，你可以通过 `allow` 和 `deny` 去设置你的规则的时候，直接在客户端进行操作通常会更简单。

然而，一旦你需要做一些在用户控制以外的事情（比如设置一个新帖子的时间戳，或者把帖子分配到正确的用户），这种情况使用内置方法会更好。

内置方法也适用在其他的一些情景：

- 当你需要通过内置方法的回调函数去获得返回值的时候，而不是等待响应和同步才传递的数据。
- 对于一些繁重的数据库操作，比如要提取大量的数据集合。
- 计算或者合计数据的时候（比如：计数、平均值、求和）。

[请阅读我们的 blog](https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/) 来深入了解这个话题。

<% end %>